package dryven.codegen.asm;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import dryven.codegen.asm.typeinfo.ArrayTypeInfo;
import dryven.codegen.asm.typeinfo.ClassTypeInfo;
import dryven.codegen.asm.typeinfo.TypeInfo;

public class MethodBuilder {
	
	private ClassBuilder cb;
	private MethodVisitor mv;
	private List<LocalVariable> _localVariables = null;
	private int _parameterCount;
	private List<TempValue> _stack = new LinkedList<TempValue>();
	private int _maxLocalVariableSlotCount = 0;
	private int _currentLocalVariableSlotCount = 0;
	private int _maxStackSlotCount = 0;
	private int _currentStackSlotCount = 0;
	
	public MethodBuilder(MethodVisitor mv, TypeInfo classType, TypeInfo returnType, TypeInfo[] paramTypes) {
		super();
		_localVariables = new ArrayList<LocalVariable>(paramTypes.length+1);
		//add 'this' local variable
		newLocalVariable(classType);
		//add parameters
		for(int i=0;i<paramTypes.length;++i) {
			newLocalVariable(paramTypes[i]);
		}
		this.mv = mv;
		_parameterCount = paramTypes.length;
	}
	
	public void finish() {
		mv.visitMaxs(_maxStackSlotCount,_maxLocalVariableSlotCount);
		mv.visitEnd();
	}
	
	public void callSuperConstructor(ReadableSymbol ... params) {
		LocalVariable superVar = new LocalVariable(0, this, getThis().getTypeInfo().getSuperType());
		superVar.invoke("<init>", Void.TYPE, params);
	}

	public void callThisConstructor(ReadableSymbol ... params) {
		getThis().invoke("<init>", Void.TYPE, params);
	}

	
	public TempValue newArray(Class<?> type, int size) {
		return newArray(new ClassTypeInfo(type), size);
	}
	
	public TempValue newArray(TypeInfo type, int size) {
		TempValue sizeValue = new Literal(size, this).read();
		if(type.isReference()) {
			mv.visitTypeInsn(Opcodes.ANEWARRAY, type.getInternalClassName());
		} else {
			int typeCode = 0;
			if(type.isChar()) {
				typeCode = Opcodes.T_CHAR;
			} else if(type.isByte()) {
				typeCode = Opcodes.T_BYTE;
			} else if(type.isBoolean()) {
				typeCode = Opcodes.T_BOOLEAN;
			} else if(type.isShort()) {
				typeCode = Opcodes.T_SHORT;
			} else if(type.isInt()) {
				typeCode = Opcodes.T_INT;
			} else if(type.isLong()) {
				typeCode = Opcodes.T_LONG;
			} else if(type.isFloat()) {
				typeCode = Opcodes.T_FLOAT;
			} else if(type.isDouble()) {
				typeCode = Opcodes.T_DOUBLE;
			} 
			mv.visitIntInsn(Opcodes.NEWARRAY, typeCode);
		}
		sizeValue.pop();
		return new TempValue(new ArrayTypeInfo(type), this);
	}
	
	public TempValue newObject(Class<?> type, ReadableSymbol ... params) {
		return newObject(new ClassTypeInfo(type), params);
	}
	
	public TempValue newObject(TypeInfo type, ReadableSymbol ... params) {
		mv.visitTypeInsn(Opcodes.NEW, type.getInternalClassName());
		TempValue a = new TempValue(type, this);
		TempValue b = a.duplicate();
		StringBuilder signature = new StringBuilder();
		signature.append('(');
		for (ReadableSymbol readableSymbol : params) {
			readableSymbol.read();
			signature.append(readableSymbol.getTypeInfo().getTypeDescriptor());
		}
		signature.append(')');
		signature.append(new ClassTypeInfo(Void.TYPE).getTypeDescriptor());
		mv.visitMethodInsn(Opcodes.INVOKESPECIAL, type.getInternalClassName(), "<init>", signature.toString());
		for(int i=0;i<params.length;++i) {
			popCurrentStackValue();
		}
		b.pop();
		return a;
	}

	public LocalVariable newLocalVariable(TypeInfo type) {
		LocalVariable lv = new LocalVariable(_currentLocalVariableSlotCount, this, type);
		_currentLocalVariableSlotCount += type.getSlotSize();
		if(_currentLocalVariableSlotCount>_maxLocalVariableSlotCount) {
			_maxLocalVariableSlotCount = _currentLocalVariableSlotCount;
		}
		_localVariables.add(lv);
		return lv;
	}
	
	public LocalVariable newLocalVariable(Class<?> type) {
		return newLocalVariable(new ClassTypeInfo(type));
	}
	
	public void returnValue(ReadableSymbol r) {
		returnValue(r.read());
	}
	
	public void returnValue(TempValue sv) {
		sv.checkValid();
		returnForType(sv.getTypeInfo());
	}
	
	public Literal createDefaultValueForType(TypeInfo type) {
		if(type.isBoolean()) {
			return new Literal(false,this);
		} else if(type.isChar()) {
			return new Literal((char)0x00,this);
		} else if(type.isByte()) {
			return new Literal((byte)0x00,this);
		} else if(type.isShort()) {
			return new Literal((byte)0x00,this);
		} else if(type.isInt()) {
			return new Literal(0,this);
		} else if(type.isFloat()) {
			return new Literal(0.0f,this);
		} else if(type.isLong()) {
			return new Literal(0l,this);
		} else if(type.isDouble()) {
			return new Literal(0.0,this);
		} else {
			try {
				return new Literal(type,this);
			} catch (ClassNotFoundException e) {
				throw new RuntimeException(e);
			}
		}
	}
	
	public void returnForType(TypeInfo type) {
		if(type.isRepresentedByInt()) {
			mv.visitInsn(Opcodes.IRETURN);
		} else if(type.isFloat()) {
			mv.visitInsn(Opcodes.FRETURN);
		} else if(type.isLong()) {
			mv.visitInsn(Opcodes.LRETURN);
		} else if(type.isDouble()) {
			mv.visitInsn(Opcodes.DRETURN);
		} else if(type.isVoid()) {
			mv.visitInsn(Opcodes.RETURN);
		} else if(type.isReference()) {
			mv.visitInsn(Opcodes.ARETURN);
		} else if(type.isVoid()) {
			mv.visitInsn(Opcodes.RETURN);
		}
		_stack.clear();
	}
	
	public void returnVoid() {
		returnForType(new ClassTypeInfo(Void.TYPE));
		_stack.clear();
	}
	
	public void throwException(TempValue v) {
		v.checkValid();
		if(!v.getTypeInfo().isAssignableTo(Throwable.class)) {
			throw new RuntimeException("Exception must inherit from Throwable");
		}
		mv.visitInsn(Opcodes.ATHROW);
		_stack.clear();
	}
	
	public LocalVariable getThis() {
		return _localVariables.get(0);
	}
	
	public LocalVariable getParameter(int index) {
		if(index>_parameterCount) {
			throw new RuntimeException("Only "+_parameterCount+" parameters");
		}
		return _localVariables.get(index+1);
	}
	
	public int getParameterCount() {
		return _parameterCount;
	}
	
	public TempValue cast(TempValue v, TypeInfo t) {
		if(t.isByte()) {
			mv.visitInsn(Opcodes.I2B);
			v.pop();
			return new TempValue(t, this);
		}
		else if(t.isChar()) {
			mv.visitInsn(Opcodes.I2C);
			v.pop();
			return new TempValue(t, this);
		}
		else if(t.isShort()) {
			mv.visitInsn(Opcodes.I2S);
			v.pop();
			return new TempValue(t, this);
		}
		else if(t.isInt()) {
			TypeInfo vt = v.getTypeInfo();
			if(vt.isFloat()) {
				mv.visitInsn(Opcodes.F2I);
				v.pop();
				return new TempValue(t, this);
			}
			else if(vt.isDouble()) {
				mv.visitInsn(Opcodes.D2I);
				v.pop();
				return new TempValue(t, this);
			}
			else if(vt.isLong()){
				mv.visitInsn(Opcodes.L2I);
				v.pop();
				return new TempValue(t, this);
			}
		}
		else if(t.isLong()) {
			TypeInfo vt = v.getTypeInfo();
			if(vt.isFloat()) {
				mv.visitInsn(Opcodes.F2L);
				v.pop();
				return new TempValue(t, this);
			}
			else if(vt.isDouble()) {
				mv.visitInsn(Opcodes.D2L);
				v.pop();
				return new TempValue(t, this);
			}
			else if(vt.isInt() || vt.isShort() || vt.isByte()){
				mv.visitInsn(Opcodes.I2L);
				v.pop();
				return new TempValue(t, this);
			}
		}
		else if(t.isFloat()) {
			TypeInfo vt = v.getTypeInfo();
			if(vt.isLong()) {
				mv.visitInsn(Opcodes.L2F);
				v.pop();
				return new TempValue(t, this);
			}
			else if(vt.isDouble()) {
				mv.visitInsn(Opcodes.D2F);
				v.pop();
				return new TempValue(t, this);
			}
			else if(vt.isInt() || vt.isShort() || vt.isByte()){
				mv.visitInsn(Opcodes.I2F);
				v.pop();
				return new TempValue(t, this);
			}
		}
		else if(t.isDouble()) {
			TypeInfo vt = v.getTypeInfo();
			if(vt.isFloat()) {
				mv.visitInsn(Opcodes.F2D);
				v.pop();
				return new TempValue(t, this);
			}
			else if(vt.isLong()) {
				mv.visitInsn(Opcodes.L2D);
				v.pop();
				return new TempValue(t, this);
			}
			else if(vt.isInt() || vt.isShort() || vt.isByte()){
				mv.visitInsn(Opcodes.I2D);
				v.pop();
				return new TempValue(t, this);
			}
		}
		else if(t.isReference() && v.getTypeInfo().isReference()) {
			mv.visitTypeInsn(Opcodes.CHECKCAST, t.getInternalClassName());
			v.pop();
			return new TempValue(t, this);
		}
		return v;
	}
	
	public TempValue add(TempValue a, TempValue b) {
		TempValue[] operands = checkArithmeticBinaryOperands(a, b);
		a = operands[0];
		b = operands[1];
		TypeInfo type = a.getTypeInfo();
		if(type.isFloat()) {
			mv.visitInsn(Opcodes.FADD);
		}
		else if(type.isDouble()) {
			mv.visitInsn(Opcodes.DADD);
		}
		else if(type.isLong()) {
			mv.visitInsn(Opcodes.LADD);
		}
		else {
			mv.visitInsn(Opcodes.IADD);
		}
		b.pop();
		a.pop();
		return new TempValue(type, this);
	}

	private TempValue[] checkArithmeticBinaryOperands(TempValue a, TempValue b) {
		a.checkValid(1);
		b.checkValid(0);
		if(a.getTypeInfo().isReference() || a.getTypeInfo().isVoid() || b.getTypeInfo().isReference() || b.getTypeInfo().isVoid()) {
			throw new RuntimeException("Can't do arithmetic with objects");
		}
		if(!a.getTypeInfo().equals(b.getTypeInfo())) {
			TypeInfo typeToCastTo = a.getTypeInfo().castPrimitiveUp(b.getTypeInfo());
			if(typeToCastTo==null) {
				throw new RuntimeException(String.format("Can't cast between types %s and %s",a.getTypeInfo().getTypeName(),b.getTypeInfo().getTypeName()));
			}
			if(typeToCastTo.equals(b.getTypeInfo())) {
				b = cast(b,typeToCastTo);
			} else {
				mv.visitInsn(Opcodes.SWAP);
				a = cast(a,typeToCastTo);
				mv.visitInsn(Opcodes.SWAP);
			}
		}
		return new TempValue[]{a,b};
	}
	
	public TempValue add(ReadableSymbol a, ReadableSymbol b) {
		return add(a.read(),b.read());
	}
	
	public TempValue sub(TempValue a, TempValue b) {
		TempValue[] operands = checkArithmeticBinaryOperands(a, b);
		a = operands[0];
		b = operands[1];
		TypeInfo type = a.getTypeInfo();
		if(type.isFloat()) {
			mv.visitInsn(Opcodes.FSUB);
		}
		else if(type.isDouble()) {
			mv.visitInsn(Opcodes.DSUB);
		}
		else if(type.isLong()) {
			mv.visitInsn(Opcodes.LSUB);
		}
		else {
			mv.visitInsn(Opcodes.ISUB);
		}
		b.pop();
		a.pop();
		return new TempValue(type, this);
	}
	
	public TempValue sub(ReadableSymbol a, ReadableSymbol b) {
		return sub(a.read(),b.read());
	}
	
	public TempValue mul(TempValue a, TempValue b) {
		TempValue[] operands = checkArithmeticBinaryOperands(a, b);
		a = operands[0];
		b = operands[1];
		TypeInfo type = a.getTypeInfo();
		if(type.isFloat()) {
			mv.visitInsn(Opcodes.FMUL);
		}
		else if(type.isDouble()) {
			mv.visitInsn(Opcodes.DMUL);
		}
		else if(type.isLong()) {
			mv.visitInsn(Opcodes.LMUL);
		}
		else {
			mv.visitInsn(Opcodes.IMUL);
		}
		b.pop();
		a.pop();
		return new TempValue(type, this);
	}
	
	public TempValue mul(ReadableSymbol a, ReadableSymbol b) {
		return mul(a.read(),b.read());
	}
	
	public TempValue div(TempValue a, TempValue b) {
		TempValue[] operands = checkArithmeticBinaryOperands(a, b);
		a = operands[0];
		b = operands[1];
		TypeInfo type = a.getTypeInfo();
		if(type.isFloat()) {
			mv.visitInsn(Opcodes.FDIV);
		}
		else if(type.isDouble()) {
			mv.visitInsn(Opcodes.DDIV);
		}
		else if(type.isLong()) {
			mv.visitInsn(Opcodes.LDIV);
		}
		else {
			mv.visitInsn(Opcodes.IDIV);
		}
		b.pop();
		a.pop();
		return new TempValue(type, this);
	}
	
	public TempValue div(ReadableSymbol a, ReadableSymbol b) {
		return div(a.read(),b.read());
	}
	
	public TempValue rem(TempValue a, TempValue b) {
		return null;
	}
	
	public TempValue rem(ReadableSymbol a, ReadableSymbol b) {
		return rem(a.read(),b.read());
	}
	
	public TempValue neg(TempValue a) {
		return null;
	}
	
	public TempValue neg(ReadableSymbol a) {
		return neg(a.read());
	}
	
	
	public TempValue inc(TempValue v) {
		return null;
	}
	
	public TempValue inc(LocalVariable v) {
		return inc(v,v);
	}
	
	protected TempValue inc(ReadableSymbol r, Assignable a) {
		a.assign(inc(r.read()));
		return null;
	}
	
	protected MethodVisitor getMethodVisitor() {
		return mv;
	}
	
	public void pushCurrentStackValue(TempValue tv) {
		_stack.add(tv);
		int slotSize = tv.getTypeInfo().getSlotSize();
		_currentStackSlotCount+=slotSize;
		if(_maxStackSlotCount<_currentStackSlotCount) {
			_maxStackSlotCount+=slotSize;
		}
	}
	
	public void popCurrentStackValue(TempValue v) {
		if(_stack.get(_stack.size()-1)!=v) {
			throw new RuntimeException("Can't pop element that is not on top");
		}
		popCurrentStackValue();
	}
	
	public void popCurrentStackValue() {
		if(_stack.isEmpty()) {
			throw new RuntimeException("Operand stack is empty, can't pop");
		}
		TempValue valueToPop = _stack.get(_stack.size()-1);
		_currentStackSlotCount -= valueToPop.getTypeInfo().getSlotSize();
		_stack.remove(_stack.size()-1);
	}
	
	public boolean isCurrentValueOnStack(TempValue tv, int position) {
		return _stack.get(_stack.size()-1-position)==tv;
	}
	
	public TempValue autoBox(ReadableSymbol r) {
		TypeInfo t = r.getTypeInfo();
		if(t.isReference()) {
			return r.read();
		}
		if(t.isByte()) {
			return newObject(Byte.class, r);
		} else if(t.isChar()) {
			return newObject(Character.class, r);
		} else if(t.isShort()) {
			return newObject(Short.class, r);
		} else if(t.isInt()) {
			return newObject(Integer.class, r);
		} else if(t.isLong()) {
			return newObject(Long.class, r);
		} else if(t.isFloat()) {
			return newObject(Float.class, r);
		} else if(t.isDouble()) {
			return newObject(Character.class, r);
		} else if(t.isBoolean()) {
			return newObject(Boolean.class, r);
		}
		throw new RuntimeException("unknown type, this is a bug");
	}
	
	public StackMapFrame newStackMapFrame() {
		return new StackMapFrame(_stack, _localVariables, this);
	}
}
